﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Collections;

namespace NabeTabetai
{
    public partial class FormSearch : Form
    {
        private static string userAgent = "User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows XP)";

        private int status = 0;
        private string outPath;
        private List<string> words = new List<string>();
        private int selectEngine = 0;
        private List<string> siteNames = new List<string>();
        private List<string> siteURLs = new List<string>();
        private List<string> siteRegexs = new List<string>();
        private int start = 0;
        private int end = 0;
        private int cnt = 0;
        private int successCnt = 0;
        private Queue<string> warnedWords = new Queue<string>();
        private Queue<string> failedWords = new Queue<string>();
        private int searchLine = 1;

        /// <summary>
        /// 検索結果URLリストの取得
        /// </summary>
        /// <param name="engine">検索エンジンを表わすリストインデックス</param>
        /// <param name="query">検索文字列</param>
        /// <returns>上位10件以下のURLリスト、もしくはnull</returns>
        private List<string> searchAndGetURLs(int engine, string query)
        {
            Encoding enc = Encoding.GetEncoding("UTF-8");
            string queryURL = siteURLs[engine];
            queryURL = queryURL.Replace("{検索文字列}", Uri.EscapeUriString(query));
            WebRequest req = WebRequest.Create(queryURL);
            WebResponse res = req.GetResponse();
            res.Headers.Add(userAgent);
            Stream stream = res.GetResponseStream();
            StreamReader reader = new StreamReader(stream, enc);
            string html = reader.ReadToEnd();
            stream.Close();
            reader.Close();
            
            List<string> urls = new List<string>();
            Regex re = new Regex(siteRegexs[engine]);
            Match m=re.Match(html);
            while (m.Success && urls.Count < 10)
            {
                string sqURL = m.Groups[1].Value;
                if (sqURL.StartsWith("http://")==false && sqURL.StartsWith("https://") == false)
                {
                    sqURL = "http://" + sqURL;
                }
                urls.Add(sqURL);
                m = m.NextMatch();
            }
            if (urls.Count > 10)
            {
                return urls.GetRange(0, 10);
            }
            else if (urls.Count <= 10)
            {
                return urls;

            }
            else
            {
                return null;
            }
        }

        /// <summary>
        ///  単語リストの読み込み
        /// </summary>
        /// <param name="path">単語リストファイルのパス</param>
        private void loadWords(string path)
        {
            FileStream stream = new FileStream(path, FileMode.Open);
            StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(932));
            string line;

            words.Clear();
            while ((line = reader.ReadLine()) != null)
            {
                words.Add(line);
            }
            reader.Close();
            stream.Close();
        }

        /// <summary>
        /// 検索エンジンリストの読み込み
        /// </summary>
        /// <param name="path">検索エンジンリストファイルのパス</param>
        private void loadSiteList(string path)
        {
            FileStream stream = new FileStream(path, FileMode.Open);
            StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(932));
            List<string> words = new List<string>();
            string line;

            try
            {
                while ((line = reader.ReadLine()) != null)
                {
                    siteNames.Add(line);
                    siteURLs.Add(reader.ReadLine());
                    siteRegexs.Add(reader.ReadLine());
                }
            }
            finally
            {
                reader.Close();
                stream.Close();
            }
        }

        public FormSearch()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (status == 0)
            {
                string msg;
                if (start < 0 || start > end || end >= words.Count)
                {
                    toolStripStatusLabel1.Text = "番号が変ですよ！";
                    return;
                }
                searchLine = int.Parse(comboBox2.Text);
                msg = "1台のPCから同時に"+searchLine+"本のHTTPリクエストを送信します。\r\n検索効率は"+searchLine+"倍になりますが、検索サイトに目を付けられるかもしれない諸刃の剣となっております。素人にはお勧めできません。\r\nよろしいですか？";
                if (searchLine>1 && MessageBox.Show(msg, "", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) == DialogResult.Cancel)
                {
                    return;
                }
                outPath = siteNames[selectEngine] + start.ToString() + "-" + end.ToString() + ".txt";
                msg = "No." + start.ToString() + "から" + "No." + end.ToString() + "までの単語を検索します。\r\n" + "検索結果のデータは " + outPath + " に保存されます。\r\n\r\n開始しますがよろしいですか？";
                if (MessageBox.Show(msg, "", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.Cancel)
                {
                    cnt = start;
                    successCnt = 0;
                    status = 1;
                    selectEngine = comboBox1.SelectedIndex;
                    toolStripStatusLabel1.Text = "検索開始...";
                    button1.Text = "キャンセル";
                    toolStripProgressBar1.Minimum = 0;
                    toolStripProgressBar1.Maximum = end - start + 1;
                    toolStripProgressBar1.Value = 0;
                    textBox1.Enabled = false;
                    textBox2.Enabled = false;
                    comboBox1.Enabled = false;
                    comboBox2.Enabled = false;
                    button2.Enabled = false;
                    warnedWords.Clear();
                    failedWords.Clear();
                    backgroundWorker1.RunWorkerAsync();
                    timer1.Enabled = true;
                    string logstr = "----検索を開始しました。----\r\n"
                        + "開始時刻: " + System.DateTime.Now.ToString("F") + "\r\n"
                        + "検索エンジン:\r\n"
                        + "    名称: " + siteNames[selectEngine] + "\r\n"
                        + "    URL: " + siteURLs[selectEngine] + "\r\n"
                        + "単語リスト:\r\n"
                        + "    ファイル名: " + textBox3.Text + "\r\n"
                        + "    開始番号: " + start.ToString() + "\r\n"
                        + "    終了番号: " + end.ToString() + "\r\n"
                        + "    語数: " + (end - start + 1).ToString() + "\r\n"
                        + "保存先:\r\n"
                        + "    ファイル名: " + outPath + "\r\n"
                        + "    検索1回あたりに保存するURL数: 10件\r\n"
                        + "検索中...\r\n";
                    textBox4.Text = textBox4.Text + logstr;
                }
            }
            else if (status == 1)
            {
                if (MessageBox.Show("キャンセルします。よろしいですか？", "INFOMATION", MessageBoxButtons.OKCancel) == DialogResult.Cancel)
                {
                    return;
                }
                button1.Enabled = false;
                toolStripStatusLabel1.Text = "キャンセル中...";
                backgroundWorker1.CancelAsync();
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            try
            {
                textBox3.Text = "words.txt";
                loadWords(textBox3.Text);
            }
            catch (FileNotFoundException)
            {
                textBox3.Text = "";
                words.Clear();
            }

            try
            {
                loadSiteList("searchEngines.txt");
                foreach (string name in siteNames)
                {
                    comboBox1.Items.Add(name);
                }
                if (siteNames.Count == 0)
                {
                    throw new Exception("searchEngines.txtが不正です。");
                }
                comboBox1.SelectedIndex = 0;
            }
            catch (FileNotFoundException)
            {
                MessageBox.Show("searchEngines.txtがありません。選択してください。", "", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                OpenFileDialog dialog = new OpenFileDialog();
                dialog.CheckFileExists = true;
                dialog.CheckPathExists = true;
                dialog.Filter = "検索エンジン設定ファイル(*.txt)|*.txt|すべてのファイル(*.*)|*.*";
                dialog.FilterIndex = 0;
                if (dialog.ShowDialog() == DialogResult.Cancel)
                {
                    throw;
                }
                loadSiteList(dialog.FileName);
                foreach (string name in siteNames)
                {
                    comboBox1.Items.Add(name);
                }
                if (siteNames.Count == 0)
                {
                    throw new Exception("searchEngines.txtが不正です。");
                }
                comboBox1.SelectedIndex = 0;
            }

            label1.Text = "開始番号(0 ～ " + (words.Count - 1).ToString() + ")";
            label2.Text = "終了番号(0 ～ " + (words.Count - 1).ToString() + ")";
        }

        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            try
            {
                start = int.Parse(textBox1.Text);
            }
            catch (Exception)
            {
                start = 0;
            }
            
            label2.Text = "終了番号(" + start.ToString() + " ～ " + (words.Count - 1) + ")";
        }

        private void textBox2_TextChanged_1(object sender, EventArgs e)
        {
            try
            {
                end = int.Parse(textBox2.Text);
            }
            catch (Exception)
            {
                end = start;
            }
 
        }

        private void searchTask(object o)
        {
            Hashtable args = (Hashtable)o;

            int no = (int)args["no"];
            int mStart = (int)args["start"];
            int mEnd = (int)args["end"];
            int count;
            int finished = 0;
            int successed = 0;
            int warned = 0;
            int failed = 0;

            Console.WriteLine(no.ToString() + " thr: [" + mStart.ToString() + "-" + mEnd.ToString() + "] word search");
            
            FileStream stream = new FileStream(outPath + ".stub" + no.ToString(), FileMode.Create);
            StreamWriter writer = new StreamWriter(stream);

            try
            {
                for (count = mStart; count <= mEnd; count++)
                {
                    List<string> urls = searchAndGetURLs(selectEngine, words[count]);

                    if (urls != null)
                    {
                        writer.Write(count.ToString());
                        foreach (string url in urls)
                        {
                            writer.Write("\t" + url);
                        }
                        writer.Write("\r\n");
                        successed++;
                        
                        if (urls.Count < 10)
                        {
                            warnedWords.Enqueue(words[count]);
                            warned++;
                        }
                    }
                    else
                    {
                        failedWords.Enqueue(words[count]);
                        failed++;
                    }
                    finished++;
                    args["finished"] = finished;
                }
            }
            finally
            {
                writer.Close();
                stream.Close();
            }
            args["successed"] = successed;
            args["wardned"] = successed;
            args["failed"] = failed;
        }

        /// <summary>
        /// 単語リストwordsのstart番からend番までを検索し、検索結果をoutPathに書き込み
        /// </summary>
        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            // スレッド準備
            List<Thread> thrs = new List<Thread>();
            List<Hashtable> args = new List<Hashtable>();
            
            for (int i = 0; i < searchLine; i++)
			{
                Thread thr = new Thread(new ParameterizedThreadStart(searchTask));
                Hashtable arg = new Hashtable();
                arg["no"] = i;
                arg["start"] = start + (end - start + 1) / searchLine * i;
                if (i == searchLine - 1)
                {
                    arg["end"] = end;
                }
                else
                {
                    arg["end"] = start + (end - start + 1) / searchLine * (i + 1) - 1;
                }
                arg["finished"] = 0;
                thrs.Add(thr);
                args.Add(arg);
                thr.Start(arg);
            }

            // スレッド実行
            bool running = true;
            while(running)
            {
                // 処理状況の更新
                int i = 0;
                foreach (Hashtable arg in args)
                {
                    i += (int)arg["finished"];
                }
                cnt = i;
                
                // 完了フラグの更新
                running = false;
                foreach (Thread thr in thrs)
                {
                    if (thr.IsAlive)
                    {
                        running = true;
                    }
                }
                
                // キャンセルの判定
                if (backgroundWorker1.CancellationPending)
                {
                    e.Cancel = true;
                    foreach (Thread thr in thrs)
                    {
                        thr.Abort();
                    }
                    return;
                }
            }
            
            successCnt = 0;
            foreach (Hashtable arg in args)
            {
                successCnt += (int)arg["successed"];
            }

            // ファイルのマージ
            FileStream stream = new FileStream(outPath, FileMode.Create);
            StreamWriter writer = new StreamWriter(stream);

            try
            {
                for (int i = 0; i < searchLine; i++)
                {
                    FileStream substream = new FileStream(outPath+".stub"+i.ToString(), FileMode.Open);
                    StreamReader reader = new StreamReader(substream);
                    string contents = reader.ReadToEnd();
                    writer.Write(contents);
                    reader.Close();
                    substream.Close();
                    File.Delete(outPath + ".stub" + i.ToString());
                }
            }
            finally
            {
                writer.Close();
                stream.Close();
            }
        }

        private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            toolStripStatusLabel1.Text = "(" + cnt.ToString() + "/" + (end - start + 1).ToString()+ ")検索中...";
            toolStripProgressBar1.Value = cnt;
            if (warnedWords.Count > 0)
            {
                textBox4.Text = textBox4.Text + "    警告: " + warnedWords.Dequeue() + " のヒット数は9件以下でした。\r\n";
            }
            if (failedWords.Count > 0)
            {
                textBox4.Text = textBox4.Text + "    失敗: " + failedWords.Dequeue() + " の検索に失敗しました。\r\n";
            }
        }

        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            timer1.Enabled = false;
            status = 0;
            if (e.Cancelled)
            {
                toolStripStatusLabel1.Text = "キャンセルしました。";
                toolStripProgressBar1.Value = 0;
                textBox4.Text = textBox4.Text + "    キャンセルされました。\r\n";
            }
            else
            {
                while (warnedWords.Count > 0)
                {
                    textBox4.Text = textBox4.Text + "    警告: " + warnedWords.Dequeue() + " のヒット数は9件以下でした。\r\n";
                }
                while (failedWords.Count > 0)
                {
                    textBox4.Text = textBox4.Text + "    失敗: " + failedWords.Dequeue() + " の検索に失敗しました。\r\n";
                }
                toolStripStatusLabel1.Text = "完了しました。";
                toolStripProgressBar1.Value = end - start + 1;
                textBox4.Text = textBox4.Text + "    完了しました。\r\n";
                textBox4.Text = textBox4.Text + (end - start + 1).ToString() + "語中、" + successCnt.ToString() + "語の検索および保存に成功しました。\r\n";
            }
            textBox1.Enabled = true;
            textBox2.Enabled = true;
            comboBox1.Enabled = true;
            comboBox2.Enabled = true;
            button2.Enabled = true;
            button1.Text = "検索";
            button1.Enabled = true;
            textBox4.Text = textBox4.Text + "終了時刻: " + System.DateTime.Now.ToString("F") + "\r\n\r\n";
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            backgroundWorker1.ReportProgress(cnt);
        }

        /// <summary>
        /// 単語ファイルの変更
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
        {
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.CheckFileExists = true;
            dialog.CheckPathExists = true;
            dialog.Multiselect = false;
            dialog.Filter = "単語リストファイル(*.txt)|*.txt|すべてのファイル(*.*)|*.*";
            dialog.FilterIndex = 0;
            if (dialog.ShowDialog() != DialogResult.Cancel)
            {
                textBox3.Text = dialog.FileName;
            }
            loadWords(textBox3.Text);
            label1.Text = "開始番号(0 ～ " + (words.Count - 1).ToString() + ")";
            label2.Text = "終了番号(0 ～ " + (words.Count - 1).ToString() + ")";
            toolStripStatusLabel1.Text = "単語リストファイルを読み込みました。";
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            selectEngine = comboBox1.SelectedIndex;
        }
        
    }
}